Skip to content

[feat] User-initiated email change flow - #2830

Merged
Will-Howard merged 14 commits into
masterfrom
wh-2810-email-change-ui
Jul 31, 2026
Merged

[feat] User-initiated email change flow#2830
Will-Howard merged 14 commits into
masterfrom
wh-2810-email-change-ui

Conversation

@Will-Howard

@Will-Howard Will-Howard commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Description

Builds on the admin-initiated version of the change-email flow (#2828), the code here should be relatively self explanatory given that. The UI is the more important thing to focus on in review.

Issue

Fixes #2570

Developer checklist

Screenshot

Account page & change email modal

State 🖥️ Desktop 📱 Mobile
Account page
Change email modal
Invalid email (client-side)
In flight
Confirmation link sent
Email already in use
Unexpected error

Confirm page

These screenshots are actually taken from #2828.

State 🖥️ Desktop
Confirm page
Success (password account)
Success (Google account)
Success (Google + password)
Success (Google-only, no password set)
Success (stale Google identity cleanup failed)
Error: link expired
Error: link no longer valid

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The account page now renders an AccountSettingsSection with change-email and change-password actions. A new ChangeEmailModal validates and submits email-change requests, displaying pending, success, conflict, and error states. Server logic centralises email validation, confirmation delivery, rate limiting, and request notices for admin and self-service mutations. Tests cover the modal, account settings integration, request authorisation, validation, token handling, and confirmation flow.

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and clearly describes the main change: a user-initiated email change flow.
Description check ✅ Passed It includes the required Description, Issue, Developer checklist, and Screenshot sections, with content filled in adequately.
Linked Issues check ✅ Passed The changes implement self-serve email changes with UI, validation, confirmation, and error handling, meeting #2570's definition of done.
Out of Scope Changes check ✅ Passed The added rate limiting, notices, tests, and confirmation flow all support the email-change feature and do not appear unrelated.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wh-2810-email-change-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Will-Howard added a commit that referenced this pull request Jul 28, 2026
onClick={() => setShowChangePasswordModal(true)}
aria-label="Change password"
>
Change Password

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Change Password" -> "Change password". From looking at recent changes I think sentence case is the direction we're going in, though it's not completely consistent

@Will-Howard

Copy link
Copy Markdown
Collaborator Author

@greptileai review @coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

The PR description images show the Account page on desktop and mobile, with the current email ada.lovelace@example.com and a new Change email button beside Change password.

They also show the responsive change-email dialog: a centred desktop modal and a mobile bottom sheet. The flow visibly covers the initial form (“We’ll send a confirmation link…”), client-side validation for not-an-email with “Please enter a valid email address”, the disabled Sending... state, and the success message confirming a link was sent to ada.new@example.com and is valid for 48 hours.

@Will-Howard: I’ll review the PR, with particular attention to the new change-email flow and its responsive UI states.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Comment thread apps/website/src/server/routers/users.ts Outdated
@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a user-initiated email-change flow.

  • Adds the account-page modal, validation, loading, success, conflict, and error states.
  • Adds an authenticated tRPC procedure that creates and sends email-change confirmation links.
  • Sends a courtesy notice to the current address and applies a per-user attempt limit.
  • Reuses the existing confirmation flow and adds frontend, router, and Customer.io tests.

Confidence Score: 3/5

This PR should not merge until concurrent email-change requests are prevented from bypassing the attempt limit.

The rate-limit slot is recorded only after asynchronous availability checking and email delivery, allowing an authenticated user to issue an arbitrary concurrent burst that exceeds the intended three-attempt cap.

Files Needing Attention: apps/website/src/server/routers/users.ts

Security Review

The new attempt limit can be bypassed with concurrent requests because checking and recording are separated by awaited operations. The rate-limit slot must be reserved atomically before checking availability or sending mail. How this was verified: The request path checks the shared attempt list, awaits both the availability lookup and external email delivery, and only then records the attempt without any per-user serialization.

Important Files Changed

Filename Overview
apps/website/src/server/routers/users.ts Adds the self-service procedure and rate limiter, but concurrent requests bypass the intended attempt cap.
apps/website/src/components/settings/ChangeEmailModal.tsx Adds the modal’s validation, submission, loading, conflict, error, and success states.
apps/website/src/lib/api/customerio.ts Adds the escaped transactional courtesy notice sent to the current email address.
apps/website/src/components/settings/AccountSettingsSection.tsx Adds the change-email entry point and incorporates it alongside password settings.
apps/website/src/lib/schemas/user/changeEmail.schema.ts Centralizes trimming, lowercasing, and validation of prospective email addresses.

Sequence Diagram

sequenceDiagram
  participant U as Authenticated user
  participant A as requestOwnEmailChange
  participant M as Attempt map
  participant E as Availability/email services
  par Concurrent request 1
    U->>A: Request email change
    A->>M: "Check attempts (< 3)"
    A->>E: Await availability and delivery
  and Concurrent request N
    U->>A: Request email change
    A->>M: "Check same unchanged attempts (< 3)"
    A->>E: Await availability and delivery
  end
  E-->>A: Complete
  A->>M: Record attempts after side effects
Loading

Reviews (2): Last reviewed commit: "[style] Drop the nothing-changes line fr..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/website/src/components/settings/AccountSettingsSection.tsx (1)

31-31: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the narrating what-comments.

{/* Account Settings Section */} and {/* Change Email Modal */} just restate what the adjacent JSX already makes obvious.

As per coding guidelines: "Write comments only when the reason is non-obvious... and remove obvious or outdated comments during refactoring."

♻️ Proposed cleanup
-      {/* Account Settings Section */}
       <div className="mb-6">
-      {/* Change Email Modal */}
       <ChangeEmailModal

Also applies to: 61-61

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/website/src/components/settings/AccountSettingsSection.tsx` at line 31,
Remove the redundant JSX comments `{/* Account Settings Section */}` and `{/*
Change Email Modal */}` from the component, leaving the surrounding JSX
structure and behavior unchanged.

Source: Coding guidelines

apps/website/src/components/settings/ChangeEmailModal.tsx (1)

1-166: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing Storybook story for a new user-facing component.

ChangeEmailModal is a new, standalone user-facing component but no accompanying Storybook story is included in this file set.

As per coding guidelines: "Add a Storybook story alongside every new user-facing component and keep stories synchronized when components are added, modified, or removed." Please confirm a story exists elsewhere, or add one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/website/src/components/settings/ChangeEmailModal.tsx` around lines 1 -
166, Add a Storybook story for the standalone ChangeEmailModal component, or
confirm and wire up an existing story if one already exists elsewhere. Cover the
open modal state and provide the required isOpen and setIsOpen props, keeping
the story synchronized with the component’s current user-facing behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/website/src/server/routers/users.ts`:
- Around line 35-55: Update sendEmailChangeConfirmation to pass the normalised
current email as oldEmail when calling createEmailChangeToken, matching the
value later validated by confirmEmailChange. Add coverage for a stored email
containing mixed case or whitespace, ensuring the confirmation flow remains
valid.

---

Nitpick comments:
In `@apps/website/src/components/settings/AccountSettingsSection.tsx`:
- Line 31: Remove the redundant JSX comments `{/* Account Settings Section */}`
and `{/* Change Email Modal */}` from the component, leaving the surrounding JSX
structure and behavior unchanged.

In `@apps/website/src/components/settings/ChangeEmailModal.tsx`:
- Around line 1-166: Add a Storybook story for the standalone ChangeEmailModal
component, or confirm and wire up an existing story if one already exists
elsewhere. Cover the open modal state and provide the required isOpen and
setIsOpen props, keeping the story synchronized with the component’s current
user-facing behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ea6b4e82-56db-4cfb-ae0e-cdd596d61f42

📥 Commits

Reviewing files that changed from the base of the PR and between 1af22b1 and eddfc57.

⛔ Files ignored due to path filters (1)
  • apps/website/src/__tests__/pages/__snapshots__/account.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (7)
  • apps/website/src/components/settings/AccountSettingsSection.test.tsx
  • apps/website/src/components/settings/AccountSettingsSection.tsx
  • apps/website/src/components/settings/ChangeEmailModal.test.tsx
  • apps/website/src/components/settings/ChangeEmailModal.tsx
  • apps/website/src/pages/account.tsx
  • apps/website/src/server/routers/users.test.ts
  • apps/website/src/server/routers/users.ts

Comment thread apps/website/src/server/routers/users.ts Outdated

@joshestein joshestein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For changing password, the user currently needs to validate their old password. I understand we can't validate a past user email in the same way, but maybe for user-initiated email changes it's worth sending a courtesy email to their old email as a form of 'validation'?

I don't think there's real risk to accounts being taken over but might still be worth letting the user know.

Comment thread apps/website/src/components/settings/AccountSettingsSection.tsx
Comment thread apps/website/src/components/settings/ChangeEmailModal.tsx Outdated
Comment thread apps/website/src/server/routers/users.ts
requestEmailChange.mutate({ newEmail: result.data });
};

const handleKeyDown = (e: React.KeyboardEvent) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use a semantic <form> do we still need this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, <form> is much better. I've changed it!

Comment thread apps/website/src/components/settings/ChangeEmailModal.tsx Outdated
@Will-Howard

Copy link
Copy Markdown
Collaborator Author

but maybe for user-initiated email changes it's worth sending a courtesy email to their old email as a form of 'validation'?

This is a good idea 👍, I'll add this

Base automatically changed from wh-2810-email-change-flow to master July 29, 2026 13:10

@marn-in-prod marn-in-prod left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting an email change notification sent to the original email would be good + not counting the user towards usage if our own infra failed. The others are mainly consistency things

Comment thread apps/website/src/server/routers/users.ts Outdated
Comment thread apps/website/src/components/settings/ChangeEmailModal.tsx Outdated
Comment thread apps/website/src/server/routers/users.ts Outdated
Comment thread apps/website/src/components/settings/AccountSettingsSection.tsx
@Will-Howard
Will-Howard temporarily deployed to wh-2810-email-change-ui - bluedot-storybook-preview PR #2830 July 30, 2026 08:36 — with Render Destroyed
@Will-Howard
Will-Howard temporarily deployed to wh-2810-email-change-ui - bluedot-preview PR #2830 July 30, 2026 08:36 — with Render Destroyed
@Will-Howard

Copy link
Copy Markdown
Collaborator Author

Here's the new courtesy email for reference:
Screenshot 2026-07-30 at 09 38 15

@Will-Howard
Will-Howard marked this pull request as ready for review July 30, 2026 08:39
Comment thread apps/website/src/server/routers/users.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/website/src/server/routers/users.ts (1)

91-106: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound the Keycloak cleanup retries or move them off the confirm-email path.

unlinkStaleGoogleIdentitiesOrAlert is awaited in confirmEmailChange, and it calls unlinkStaleGoogleIdentities 3 times sequentially. adminRequest uses axios.request(...) without a timeout; Axios request timeout defaults to 0, so a slow/unresponsive Keycloak admin client can make the user’s confirm link hang indefinitely. Since stale Google identity cleanup is best-effort, cap the total retry wall-clock time or make the cleanup fire-and-forget.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/website/src/server/routers/users.ts` around lines 91 - 106, Update
unlinkStaleGoogleIdentitiesOrAlert and its confirmEmailChange call path so stale
Google identity cleanup cannot block indefinitely: add a bounded timeout
covering each Keycloak admin request and the retry sequence, or dispatch the
best-effort cleanup without awaiting it. Preserve the existing retry and
slack-alert behavior for failures while ensuring confirmEmailChange completes
even when Keycloak is slow or unresponsive.
🧹 Nitpick comments (4)
apps/website/src/components/settings/ChangeEmailModal.tsx (2)

60-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated fixed-width modal spacer hack. Both modals use an invisible <div className="h-0 w-[600px] max-w-full" /> purely to force the modal's rendered width; the same effect can be achieved directly with min-w-[600px] max-w-full on the content wrapper, which also better matches the guideline of preferring minimum over fixed dimensions.

  • apps/website/src/components/settings/ChangeEmailModal.tsx#L60-L61: replace the spacer div with min-w-[600px] max-w-full on the <div className="w-full max-w-modal"> wrapper (or a shared modal-width helper).
  • apps/website/src/components/settings/AccountSettingsSection.tsx#L192-L193: apply the same change to ChangePasswordModal's wrapper, ideally sharing one utility/class between both modals to prevent the two widths drifting apart over time.

As per coding guidelines: "Prefer min-h-[Xpx] over h-[Xpx], use minimum dimensions for flexibility, and avoid maximum dimensions except max-w-prose."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/website/src/components/settings/ChangeEmailModal.tsx` around lines 60 -
61, The modal width is forced by duplicated invisible fixed-width spacer
elements. In apps/website/src/components/settings/ChangeEmailModal.tsx lines
60-61, move the 600px minimum width onto the content wrapper and remove the
spacer; apply the same change to ChangePasswordModal in
apps/website/src/components/settings/AccountSettingsSection.tsx lines 192-193,
preferably through a shared utility/class, while preserving responsive
full-width behavior.

Source: Coding guidelines


1-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a Storybook story for ChangeEmailModal.

ChangeEmailModal is a new user-facing component, but no matching story file is present under apps/website/src/components/settings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/website/src/components/settings/ChangeEmailModal.tsx` around lines 1 -
154, Add a Storybook story file for the ChangeEmailModal component under the
settings components stories, configuring the required isOpen and setIsOpen props
and providing a representative default interaction state.

Source: Coding guidelines

apps/website/src/lib/api/customerio.ts (1)

130-177: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the shared fetch/error-handling boilerplate.

sendEmailChangeVerification and sendEmailChangeRequestedNotice duplicate the same POST-to-/send/email request/response handling, differing only in to, subject, and body. A small shared helper (e.g. postTransactionalEmail({ to, identifierEmail, subject, body })) would reduce duplication and keep future error-handling changes in one place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/website/src/lib/api/customerio.ts` around lines 130 - 177, The
duplicated Customer.io POST and response handling in sendEmailChangeVerification
and sendEmailChangeRequestedNotice should be extracted into a shared helper such
as postTransactionalEmail accepting recipient, identifier email, subject, and
body. Update both functions to build their message-specific values and delegate
the request and HTTP error handling to that helper, preserving their existing
payloads and error behavior.
apps/website/src/server/routers/users.ts (1)

35-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use env for the confirmation URL base.

apps/website/src/server/routers/users.ts:36 still reads process.env.NEXT_PUBLIC_SITE_URL directly, while this file already goes through env for server-side values. Expose NEXT_PUBLIC_SITE_URL via env or add a small server-side config helper so this helper does not bypass central env handling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/website/src/server/routers/users.ts` around lines 35 - 38, Update
getEmailChangeConfirmUrl to obtain the confirmation URL base through the
existing env configuration rather than reading process.env.NEXT_PUBLIC_SITE_URL
directly. Expose NEXT_PUBLIC_SITE_URL through env or reuse a small server-side
configuration helper, while preserving the current default URL and token
encoding behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/website/src/server/routers/users.ts`:
- Around line 60-87: Replace the process-local emailChangeAttemptsByUserId Map
used by assertWithinEmailChangeRateLimit, recordEmailChangeAttempt, and
resetEmailChangeRateLimits with a shared persistent store such as Redis or a
database-backed record with expiration. Ensure checks and recording are atomic
across instances, prune expired attempts independently of new checks, and
preserve the existing 3-attempts-per-30-minute behavior and error response.

---

Outside diff comments:
In `@apps/website/src/server/routers/users.ts`:
- Around line 91-106: Update unlinkStaleGoogleIdentitiesOrAlert and its
confirmEmailChange call path so stale Google identity cleanup cannot block
indefinitely: add a bounded timeout covering each Keycloak admin request and the
retry sequence, or dispatch the best-effort cleanup without awaiting it.
Preserve the existing retry and slack-alert behavior for failures while ensuring
confirmEmailChange completes even when Keycloak is slow or unresponsive.

---

Nitpick comments:
In `@apps/website/src/components/settings/ChangeEmailModal.tsx`:
- Around line 60-61: The modal width is forced by duplicated invisible
fixed-width spacer elements. In
apps/website/src/components/settings/ChangeEmailModal.tsx lines 60-61, move the
600px minimum width onto the content wrapper and remove the spacer; apply the
same change to ChangePasswordModal in
apps/website/src/components/settings/AccountSettingsSection.tsx lines 192-193,
preferably through a shared utility/class, while preserving responsive
full-width behavior.
- Around line 1-154: Add a Storybook story file for the ChangeEmailModal
component under the settings components stories, configuring the required isOpen
and setIsOpen props and providing a representative default interaction state.

In `@apps/website/src/lib/api/customerio.ts`:
- Around line 130-177: The duplicated Customer.io POST and response handling in
sendEmailChangeVerification and sendEmailChangeRequestedNotice should be
extracted into a shared helper such as postTransactionalEmail accepting
recipient, identifier email, subject, and body. Update both functions to build
their message-specific values and delegate the request and HTTP error handling
to that helper, preserving their existing payloads and error behavior.

In `@apps/website/src/server/routers/users.ts`:
- Around line 35-38: Update getEmailChangeConfirmUrl to obtain the confirmation
URL base through the existing env configuration rather than reading
process.env.NEXT_PUBLIC_SITE_URL directly. Expose NEXT_PUBLIC_SITE_URL through
env or reuse a small server-side configuration helper, while preserving the
current default URL and token encoding behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fd1ac857-e35e-44dd-9079-82b2e3bba55c

📥 Commits

Reviewing files that changed from the base of the PR and between eddfc57 and cd4cc5b.

⛔ Files ignored due to path filters (1)
  • apps/website/src/__tests__/pages/__snapshots__/account.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (10)
  • apps/website/src/components/settings/AccountSettingsSection.test.tsx
  • apps/website/src/components/settings/AccountSettingsSection.tsx
  • apps/website/src/components/settings/ChangeEmailModal.test.tsx
  • apps/website/src/components/settings/ChangeEmailModal.tsx
  • apps/website/src/lib/api/customerio.test.ts
  • apps/website/src/lib/api/customerio.ts
  • apps/website/src/lib/schemas/user/changeEmail.schema.ts
  • apps/website/src/pages/account.tsx
  • apps/website/src/server/routers/users.test.ts
  • apps/website/src/server/routers/users.ts

Comment thread apps/website/src/server/routers/users.ts
@marn-in-prod

marn-in-prod commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Here's the new courtesy email for reference: Screenshot 2026-07-30 at 09 38 15

Looks good, but seperately, the email addressed attached to email changes isn't a noreply email address right? It's one where Bluedot folks can easily see messages from?

I'm asking because the last time I reset my password, the email came from "noreply@bluedot.org"

@Will-Howard

@Will-Howard

Copy link
Copy Markdown
Collaborator Author

Looks good, but seperately, the email addressed attached to email changes isn't a noreply email address right?

It uses team@bluedot.org, which is the general contact email we use. I also just sent an email to check that it's monitored. I won't wait for that before shipping though, it can be fixed after the fact if not.

@Will-Howard
Will-Howard merged commit db45633 into master Jul 31, 2026
14 checks passed
@Will-Howard
Will-Howard deleted the wh-2810-email-change-ui branch July 31, 2026 13:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow users to change emails

3 participants